fix: resolve skills-core.js path using home directory for Windows compatibility#326
fix: resolve skills-core.js path using home directory for Windows compatibility#326
Conversation
1a0d0b3 to
492bf99
Compare
📝 WalkthroughWalkthroughReplaces a static import of Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Plugin as "Plugin (.opencode/plugin/superpowers.js)"
participant Env as "Environment / OS"
participant FS as "Filesystem"
participant SkillsCore as "skills-core (dynamic module)"
Plugin->>Env: read env vars (e.g., OPENCODE_CONFIG_DIR, HOME)
Env-->>Plugin: resolved superpowersRoot / paths
Plugin->>FS: compute absolute skillsCore path + pathToFileURL
FS-->>Plugin: file URL for import
Plugin->>SkillsCore: dynamic import(fileURL)
SkillsCore-->>Plugin: exported helpers (resolveSkillPath, stripFrontmatter, ...)
Plugin->>Plugin: initialize using resolvedSkillsCore functions
(Note: colored rectangles not required for this simple flow.) Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.opencode/plugin/superpowers.js (1)
42-43:superpowersSkillsDirstill uses__dirname-relative path.The PR fixes the
skills-core.jsimport by using a hardcoded home-directory path, butsuperpowersSkillsDirstill derives from__dirnamewith../../skills. If the plugin file is copied (the same Windows scenario the PR aims to fix), this path will resolve incorrectly (e.g.,~/.config/opencode/plugin/../../skills→~/.config/skills).Consider using the same
superpowersRootfor consistency:🛠️ Suggested fix: use superpowersRoot for skills directory
const homeDir = os.homedir(); const projectSkillsDir = path.join(directory, '.opencode/skills'); - // Derive superpowers skills dir from plugin location (works for both symlinked and local installs) - const superpowersSkillsDir = path.resolve(__dirname, '../../skills'); + // Use the same superpowersRoot for skills directory (works for both symlinked and copied installs) + const superpowersSkillsDir = path.join(superpowersRoot, 'skills');
🤖 Fix all issues with AI agents
In @.opencode/plugin/superpowers.js:
- Around line 16-22: Wrap the dynamic top-level import of skills-core.js in a
try-catch around the import(pathToFileURL(skillsCorePathResolved).href) call:
catch the error, log a clear, actionable message that includes
skillsCorePathResolved and the caught error (use existing logger or
console.error), and either rethrow or gracefully disable the plugin depending on
desired behavior; ensure this handling is applied where homeDir,
superpowersRoot, skillsCorePathResolved and skillsCore are defined so the plugin
fails with a helpful message instead of an unhandled exception.
- Around line 19-22: The code hardcodes superpowersRoot and loads skills-core.js
from ~/.config/opencode/superpowers which ignores OPENCODE_CONFIG_DIR; update
the resolution to reuse the same config-dir logic used later (the
OPENCODE_CONFIG_DIR fallback) so that superpowersRoot is derived from that
resolved config directory, then rebuild skillsCorePathResolved and import
skillsCore from that path; ensure you reference the same environment variable
handling used elsewhere so loading respects OPENCODE_CONFIG_DIR.
🧹 Nitpick comments (1)
.opencode/plugin/superpowers.js (1)
19-20: DuplicatehomeDirdeclaration.
homeDiris declared at module scope (line 19) and again insideSuperpowersPlugin(line 40). The inner declaration shadows the outer one and both callos.homedir(). Consider removing the duplicate and reusing the module-level constant.♻️ Suggested refactor
export const SuperpowersPlugin = async ({ client, directory }) => { - const homeDir = os.homedir(); const projectSkillsDir = path.join(directory, '.opencode/skills');Also applies to: 40-40
| // Resolve skills-core.js path dynamically to work with both symlinks and copies | ||
| // On Mac/Linux with symlink: __dirname points to actual location | ||
| // On Windows with copy: we need to find the superpowers installation | ||
| const homeDir = os.homedir(); | ||
| const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers'); | ||
| const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js'); | ||
| const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href); |
There was a problem hiding this comment.
Missing error handling for dynamic import.
The top-level await import() will throw an unhandled error if skills-core.js doesn't exist at the resolved path (e.g., fresh install, non-standard installation). This causes the entire plugin to fail to load with an unhelpful error message.
🛠️ Suggested fix: wrap in try-catch with helpful error
// Resolve skills-core.js path dynamically to work with both symlinks and copies
// On Mac/Linux with symlink: __dirname points to actual location
// On Windows with copy: we need to find the superpowers installation
const homeDir = os.homedir();
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');
-const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
+
+let skillsCore;
+try {
+ skillsCore = await import(pathToFileURL(skillsCorePathResolved).href);
+} catch (err) {
+ throw new Error(
+ `Failed to load skills-core.js from ${skillsCorePathResolved}. ` +
+ `Ensure superpowers is installed at ${superpowersRoot}. ` +
+ `Original error: ${err.message}`
+ );
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Resolve skills-core.js path dynamically to work with both symlinks and copies | |
| // On Mac/Linux with symlink: __dirname points to actual location | |
| // On Windows with copy: we need to find the superpowers installation | |
| const homeDir = os.homedir(); | |
| const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers'); | |
| const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js'); | |
| const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href); | |
| // Resolve skills-core.js path dynamically to work with both symlinks and copies | |
| // On Mac/Linux with symlink: __dirname points to actual location | |
| // On Windows with copy: we need to find the superpowers installation | |
| const homeDir = os.homedir(); | |
| const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers'); | |
| const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js'); | |
| let skillsCore; | |
| try { | |
| skillsCore = await import(pathToFileURL(skillsCorePathResolved).href); | |
| } catch (err) { | |
| throw new Error( | |
| `Failed to load skills-core.js from ${skillsCorePathResolved}. ` + | |
| `Ensure superpowers is installed at ${superpowersRoot}. ` + | |
| `Original error: ${err.message}` | |
| ); | |
| } |
🤖 Prompt for AI Agents
In @.opencode/plugin/superpowers.js around lines 16 - 22, Wrap the dynamic
top-level import of skills-core.js in a try-catch around the
import(pathToFileURL(skillsCorePathResolved).href) call: catch the error, log a
clear, actionable message that includes skillsCorePathResolved and the caught
error (use existing logger or console.error), and either rethrow or gracefully
disable the plugin depending on desired behavior; ensure this handling is
applied where homeDir, superpowersRoot, skillsCorePathResolved and skillsCore
are defined so the plugin fails with a helpful message instead of an unhandled
exception.
| const homeDir = os.homedir(); | ||
| const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers'); | ||
| const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js'); | ||
| const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href); |
There was a problem hiding this comment.
Inconsistency: hardcoded path ignores OPENCODE_CONFIG_DIR environment variable.
The superpowersRoot path is hardcoded to ~/.config/opencode/superpowers, but later (lines 45-46) the plugin respects OPENCODE_CONFIG_DIR for the config directory. If a user sets this env var, skills-core.js will still be loaded from the default location, causing a mismatch.
Consider using the same resolution logic:
🛠️ Suggested fix: respect OPENCODE_CONFIG_DIR
+// Helper to normalize path (needed before plugin init)
+const normalizePathEarly = (p, home) => {
+ if (!p || typeof p !== 'string') return null;
+ let normalized = p.trim();
+ if (!normalized) return null;
+ if (normalized.startsWith('~/')) {
+ normalized = path.join(home, normalized.slice(2));
+ } else if (normalized === '~') {
+ normalized = home;
+ }
+ return path.resolve(normalized);
+};
+
const homeDir = os.homedir();
-const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
+const envConfigDir = normalizePathEarly(process.env.OPENCODE_CONFIG_DIR, homeDir);
+const superpowersRoot = path.join(envConfigDir || path.join(homeDir, '.config/opencode'), 'superpowers');
const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const homeDir = os.homedir(); | |
| const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers'); | |
| const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js'); | |
| const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href); | |
| // Helper to normalize path (needed before plugin init) | |
| const normalizePathEarly = (p, home) => { | |
| if (!p || typeof p !== 'string') return null; | |
| let normalized = p.trim(); | |
| if (!normalized) return null; | |
| if (normalized.startsWith('~/')) { | |
| normalized = path.join(home, normalized.slice(2)); | |
| } else if (normalized === '~') { | |
| normalized = home; | |
| } | |
| return path.resolve(normalized); | |
| }; | |
| const homeDir = os.homedir(); | |
| const envConfigDir = normalizePathEarly(process.env.OPENCODE_CONFIG_DIR, homeDir); | |
| const superpowersRoot = path.join(envConfigDir || path.join(homeDir, '.config/opencode'), 'superpowers'); | |
| const skillsCorePathResolved = path.join(superpowersRoot, 'lib/skills-core.js'); | |
| const skillsCore = await import(pathToFileURL(skillsCorePathResolved).href); |
🤖 Prompt for AI Agents
In @.opencode/plugin/superpowers.js around lines 19 - 22, The code hardcodes
superpowersRoot and loads skills-core.js from ~/.config/opencode/superpowers
which ignores OPENCODE_CONFIG_DIR; update the resolution to reuse the same
config-dir logic used later (the OPENCODE_CONFIG_DIR fallback) so that
superpowersRoot is derived from that resolved config directory, then rebuild
skillsCorePathResolved and import skillsCore from that path; ensure you
reference the same environment variable handling used elsewhere so loading
respects OPENCODE_CONFIG_DIR.
…patibility Fixes obra#303 On Windows, when using ln -sf (which behaves like cp in Git Bash), the relative import path '../../lib/skills-core.js' fails because it resolves from ~/.config/opencode/plugin/ to an incorrect location. This change uses os.homedir() to construct an absolute path to the superpowers installation directory, then locates skills-core.js from there. This works correctly on all platforms regardless of whether the plugin file is symlinked or copied. Tested on: - Windows 11 with OpenCode (copied file via Git Bash ln -sf) - Works on Mac/Linux with symlinks (cross-platform path.join)
492bf99 to
1a0959e
Compare
AnalysisThis PR attempts to fix #303 / #232 (Windows module resolution failure) by hardcoding the superpowers installation path. Problem with this approachThe fix changes: // Before: relative import (works with proper symlink)
import * as skillsCore from '../../lib/skills-core.js';
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
// After: hardcoded path (only works with specific install location)
const superpowersRoot = path.join(homeDir, '.config/opencode/superpowers');
const skillsCore = await import(pathToFileURL(path.join(superpowersRoot, 'lib/skills-core.js')).href);
const superpowersSkillsDir = path.join(superpowersRoot, 'skills');This breaks flexibility:
Root CauseThe real problem isn't the code - it's that Git Bash Better Solutions
Tracking IssueSee #232 for the canonical tracking of this problem. I recommend not merging this PR as-is because it trades one problem (Windows install broken) for another (all non-standard installs broken). |
|
Closing in favor of PR #330. That PR fixes the Windows issue by:
This avoids the hardcoded path approach which would have broken project-local and custom installations. |
Summary
Fixes #303
Resolves the "Cannot find module '../../lib/skills-core.js'" error on Windows by using absolute paths instead of relative imports.
Motivation and Context
On Windows, when using
ln -sfin Git Bash (which behaves likecpinstead of creating a symlink), the plugin file is copied rather than symlinked. This causes relative import paths to resolve incorrectly:Problem 1 - skills-core.js import:
~/.config/opencode/plugin/superpowers.js../../lib/skills-core.jsresolves to:~/.config/lib/skills-core.js❌~/.config/opencode/superpowers/lib/skills-core.js✓Problem 2 - skills directory:
../../skillsresolves to:~/.config/skills/❌~/.config/opencode/superpowers/skills/✓Error messages:
This prevents the OpenCode plugin from loading entirely on Windows.
How Has This Been Tested?
find_skillstool returns correct skill listuse_skilltool works correctlyln -sf) and manual symlink scenariosos.homedir(),path.join(), andpathToFileURL()which work on Mac/Linux/WindowsCode Changes
Before fix:
After fix:
Key changes:
os.homedir()to get platform-independent home directorypathToFileURL()to convert file path to proper URL for dynamic import (required on Windows)Breaking Changes
None. This is a bug fix that maintains backward compatibility with Unix systems while adding Windows support.
Types of changes
Checklist
Additional context
This issue affects all Windows users trying to install superpowers via the documented installation instructions. The fix uses platform-independent Node.js APIs:
os.homedir()- Returns correct home directory on all platformspath.join()- Handles path separators correctly (/ vs )pathToFileURL()- Converts absolute paths to file:// URLs for dynamic importThe solution works correctly regardless of:
The change is minimal (11 lines added, 4 removed) and focused solely on the import resolution issue.